package com.wmm.fast.order.service;

import com.fast.tcc.exception.PartialConfirmException;
import com.fast.tcc.exception.ReservationExpireException;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.netflix.hystrix.exception.HystrixRuntimeException;
import com.wmm.common.Shift;
import com.wmm.common.bean.ResponseResultDTO;
import com.wmm.common.bean.user.User;
import com.wmm.common.persistence.CrudMapper;
import com.wmm.common.service.CrudServiceImpl;
import com.wmm.common.util.OrikaMapper;
import com.wmm.fast.order.controller.StatusCode;
import com.wmm.fast.order.controller.client.ProductClient;
import com.wmm.fast.order.controller.client.TccClient;
import com.wmm.fast.order.controller.client.UserClient;
import com.wmm.fast.order.dao.OrderParticipantMapper;
import com.wmm.fast.order.domain.Order;
import com.wmm.fast.order.domain.OrderConflict;
import com.wmm.fast.order.domain.OrderParticipant;
import com.wmm.fast.order.domain.type.OrderStatus;
import com.wmm.fast.order.model.Participant;
import com.wmm.fast.order.model.Product;
import com.wmm.fast.order.model.TccRequest;
import com.wmm.fast.order.model.request.BalanceReservationRequest;
import com.wmm.fast.order.model.request.PaymentRequest;
import com.wmm.fast.order.model.request.PlaceOrderRequest;
import com.wmm.fast.order.model.request.StockReservationRequest;
import com.wmm.fast.order.model.response.ReservationResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
 * @Auther: wangzun
 * @Date: 2018/9/14 16:48
 * @Description:
 */
@Service
@Slf4j
public class OrderService extends CrudServiceImpl<Order> {

    @Autowired
    public OrderService(CrudMapper<Order> mapper) {
        super(mapper);
    }
    @Autowired
    private ProductClient productClient;
    @Autowired
    private UserClient userClient;
    @Autowired
    private TccClient tccClient;
    @Autowired
    private OrderConflictService conflictService;

    @Autowired
    private OrderParticipantMapper participantMapper;


    @Transactional(rollbackFor = {Exception.class,RuntimeException.class})
    public ResponseResultDTO<Order> placeOrder(PlaceOrderRequest request) {
        Preconditions.checkNotNull(request);
        final Long userId = Preconditions.checkNotNull(request.getUserId());
        final Long productId = Preconditions.checkNotNull(request.getProductId());
        //调用product-ms 获取产品
        final Product product = findRemoteProduct(productId);
        //调用user-ms查询用户
        final User user = findRemoteUser(userId);
        //检查余额
        if(user.getBalance().doubleValue() - product.getPrice() < 0) {
            Shift.fatal(StatusCode.INSUFFICIENT_BALANCE);
        }
        //构建订单
        final Order order = new Order();
        order.setUserId(userId);
        order.setProductId(productId);
        order.setStatus(OrderStatus.PROCESSING);
        super.persistNonNullProperties(order);
        //此处开始tcc-try. 若此处异常则说明product/user 服务业务异常,带动order rollback.
        //预留库存
        reserveProductAndPersistParticipant(order);
        //预留余额
        reserveBalanceAndPersistParticipant(order);
        return new ResponseResultDTO<>(order);
    }

    private void reserveBalanceAndPersistParticipant(Order order) {
        Preconditions.checkNotNull(order);
        final ReservationResponse balanceResponse = reserveBalance(order);
        final Participant participant = balanceResponse.getParticipant();
        if(participant == null) {
            Shift.fatal(StatusCode.INSUFFICIENT_BALANCE);
        }
        persistParticipant(participant,order.getId());
    }

    private ReservationResponse
    reserveBalance(Order order) {
        final BalanceReservationRequest reservation = new BalanceReservationRequest();
        reservation.setUserId(order.getUserId());
        return userClient.reserve(reservation);
    }

    /** 调用tcc-try 执行业务检查,预留业务资源(事务隔离) */
    private void reserveProductAndPersistParticipant(Order order) {
        Preconditions.checkNotNull(order);
        final ReservationResponse stockResponse = reserveProduct(order);
        //根据返回结果判断是否try失败
        final Participant participant = stockResponse.getParticipant();
        if(participant == null) {
            Shift.fatal(StatusCode.INSUFFICIENT_PRODUCT);
        }
        persistParticipant(participant,order.getId());
    }

    /** 资源管理持久化库采用Mysql */
    private void persistParticipant(Participant participant, Long id) {
        Preconditions.checkNotNull(participant);
        Preconditions.checkNotNull(id);
        final OrderParticipant orderParticipant = OrikaMapper.map(participant,OrderParticipant.class);
        orderParticipant.setOrderId(id);
        participantMapper.insertSelective(orderParticipant);
    }

    private ReservationResponse reserveProduct(Order order) {
        final StockReservationRequest reservation = new StockReservationRequest();
        reservation.setProductId(order.getProductId());
        return productClient.reserve(reservation);
    }

    private User findRemoteUser(Long userId) {
        final User user = userClient.findUser(userId).getData();
        if(user == null) {
            Shift.fatal(StatusCode.USER_NOT_EXISTS);
        }
        return user;
    }

    private Product findRemoteProduct(Long productId) {
        final Product product = productClient.findProduct(productId).getData();
        if(product == null) {
            Shift.fatal(StatusCode.PRODUCT_NOT_EXISTS);
        }

        if(product.getStock() <= 0) {
            Shift.fatal(StatusCode.INSUFFICIENT_PRODUCT);
        }
        return product;
    }

    @Transactional(rollbackFor = {Exception.class,RuntimeException.class})
    public ResponseResultDTO<Order> confirmOrder(PaymentRequest request) {
        Preconditions.checkNotNull(request);
        final Long orderId = request.getOrderId();
        //检查订单是否存在
        final Order order = super.find(orderId);
        if(order == null) {
            Shift.fatal(StatusCode.ORDER_NOT_EXISTS);
        }
        //根据orderid找到此次tcc事务预留的资源.由此可见mysql库并非最好的方式做资源持久化. 最好选择k-v库
        final List<OrderParticipant> participants = participantMapper.selectByOrderId(orderId);
        if(participants.size() == 0) {
            log.info("order id `{}` does not reserve any resource",orderId);
            Shift.fatal(StatusCode.SERVER_UNKNOWN_ERROR);
        }
        if(order.getStatus() == OrderStatus.PROCESSING) {
            confirmPhase(order,participants);
        }
        return new ResponseResultDTO<>(order);
    }

    /**  调用TCC-confirm执行资源确认 */
    private void confirmPhase(Order order, List<OrderParticipant> participants) {
        Preconditions.checkNotNull(order);
        Preconditions.checkNotNull(participants);
        Preconditions.checkArgument(!participants.isEmpty());
        //不为空表示全部try成功.现在进行确认操作
        final ImmutableList<OrderParticipant> links = ImmutableList.copyOf(participants);
        final TccRequest request = new TccRequest(links);
        try {
            tccClient.confirm(request);
            order.setStatus(OrderStatus.DONE);
            super.updateNonNullProperties(order);
        }catch (HystrixRuntimeException e) {
            final Class<? extends Throwable> exctionsClass = e.getCause().getClass();
            if(ReservationExpireException.class.isAssignableFrom(exctionsClass)) {
                //所有confirm ->try的资源预留都已经超时
                order.setStatus(OrderStatus.TIMEOUT);
            }else if(PartialConfirmException.class.isAssignableFrom(exctionsClass)) {
                //资源确认产生冲突情况,部分成功,部分失败此时需要人工介入.
                order.setStatus(OrderStatus.CONFLICT);
                makeDownConfliction(order,e);
            }else {
                throw e;
            }
            super.updateNonNullProperties(order);
        }

    }

    /** 标记下资源冲突日志, 留待人工介入 */
    private void makeDownConfliction(Order order,HystrixRuntimeException e) {
        Preconditions.checkNotNull(order);
        Preconditions.checkNotNull(e);

        final String message = e.getCause().getMessage();
        log.error("order id `{}` has some an confliction. {}",order.getId(),message);
        final OrderConflict conflict = new OrderConflict();
        conflict.setOrderId(order.getId());
        conflict.setErrorDetail(message);
        conflictService.persistNonNullProperties(conflict);
    }
}
